import re
import logging
import datetime
import gdata.projecthosting.client
import gdata.projecthosting.data
import gdata.gauth
import gdata.client
import gdata.data
import atom.http_core
import atom.core
from google.appengine.ext import db
from google.appengine.ext.db import GqlQuery
from google.appengine.api import taskqueue
from google.appengine.api.taskqueue import TaskAlreadyExistsError

# Regular expressions for matching labels
labels = {
    'category': re.compile('Category-(.+)'),
    'estimated_time': re.compile('EstimatedTime-(.+)'),
    'milestone': re.compile('Milestone-(.+)'),
    'priority': re.compile('Priority-(.+)'),
    'phase': re.compile('Phase-(.+)'),
    'percent_complete': re.compile('PercentComplete-(.+)'),
}

integer_labels = ['percent_complete', ]

OPEN = ('New', 'Accepted', 'Started' )
STARTED = ('Started', )
COMPLETED = ['Fixed', 'Verified', 'Done']
NOT_TRACKING = ('Duplicate', 'Invalid', 'WontFix')

CAT_DENOMALIZED_PROPERTIES = {
    'open_issues': OPEN,
    'completed_issues': COMPLETED,
    'started_issues': STARTED,
}


class Project(db.Model):
    name = db.StringProperty(required=True)
    latest_issue_update = db.DateTimeProperty(required=False)
    contributors = db.StringListProperty(default=[])
    num_tickets = db.IntegerProperty(default=0)
    
    def __str__(self):
        return self.name
    
    def tickets(self):
        return Ticket.gql('WHERE project = :1', self.name)
    
    def milestones(self):
        return Milestone.gql('WHERE project = :1', self)
    
    def committers(self):
        l = []
        for contributor in self.contributors:
            committers = Committer.gql('WHERE aliases = :1', contributor)
            if committers.count() > 0:
                l.append(committers[0])
            else:
                c = Committer(name=contributor, aliases=[contributor])
                c.put()
                l.append(c)
        return l
            
    def reload_tickets(self):
        db.delete(self.tickets())
        client = gdata.projecthosting.client.ProjectHostingClient()
        query = gdata.projecthosting.client.Query(max_results='10000')
        feed = client.get_issues(self.name, query=query)
        logging.debug('got feed')
        self.num_tickets = 0
        modified_categories = []
        for issue in feed.entry:
            ticket = self._ticket_from_issue(issue)
            if ticket.status not in NOT_TRACKING:
                ticket.put()
            self._find_new_owners(ticket)
            cat = self._create_or_update_category(ticket)
            if cat != None and cat not in modified_categories:
                modified_categories.append(cat)
            self.num_tickets = self.num_tickets + 1
        self.latest_issue_update = datetime.datetime.now()
        self.put()
        for cat in modified_categories:
            try:
                taskqueue.add(
                    url='/categories/%s/' % (cat.key(), ), 
                    name="calc-hours-%s" % (cat.name))
            except TaskAlreadyExistsError:
                pass
        
    def delete(self, *args, **kwargs):
        db.delete(self.tickets())
        db.delete(self.milestones())
        db.delete(self.categories())
        super(Project, self).delete(*args, **kwargs)
    
    def categories(self):
        return Category.gql('WHERE project = :1', self)
    
    def update_tickets(self):
        client = gdata.projecthosting.client.ProjectHostingClient()
        query_time = datetime.datetime.now()
        last_update = self.latest_issue_update or query_time - datetime.timedelta(days=1825)
        query = gdata.projecthosting.client.Query(max_results='10000', 
            updated_min=last_update.isoformat())
        feed = client.get_issues(self.name, query=query)
        updated = 0
        modified_categories = []
        for issue in feed.entry:
            ticket = self._ticket_from_issue(issue)
            old_ticket = Ticket.gql('WHERE id = :1', ticket.id)
            if old_ticket.count() > 0:
                # update
                db.delete(old_ticket)
            else:
                # new ticket
                self.num_tickets = self.num_tickets + 1                
            if ticket.status not in NOT_TRACKING:
                ticket.put()
            self._find_new_owners(ticket)
            cat = self._create_or_update_category(ticket)
            if cat != None and cat not in modified_categories:
                modified_categories.append(cat)
            updated = updated + 1
        if updated > 0 or self.latest_issue_update is None:
            self.latest_issue_update = query_time
        self.put()
        for cat in modified_categories:
            try:
                taskqueue.add(
                    url='/categories/%s/' % (cat.key(), ), 
                    name="calc-hours-%s" % (cat.name))
            except TaskAlreadyExistsError:
                pass
        return updated
                
    def _find_new_owners(self, ticket):
        if self.contributors is None:
            self.contributors = []
            self.put()
        if ticket.owner and ticket.owner not in self.contributors:
            self.contributors.append(ticket.owner)
            self.put()

    def _ticket_from_issue(self, issue):
        owner = None
        if issue.owner:
            owner = issue.owner.username.text
        params = {
            'id': issue.get_alternate_link().href,
            'project': self.name,
            'owner': owner,
            'summary': issue.title.text,
            'description': issue.content.text,
            'status': issue.status.text,
            'labels': [label.text for label in issue.label],
            'updated': datetime.datetime.strptime(issue.updated.text, "%Y-%m-%dT%H:%M:%S.000Z"),
        }
        for label in params['labels']:
            for key in labels.keys():
                match = labels[key].match(label)
                if match:
                    if(key in integer_labels):
                        params[key] = int(match.groups()[0])
                    else:
                        params[key] = match.groups()[0]
        if 'estimated_time' in params.keys():
            try:
                params['estimated_time'] = int(params['estimated_time'])
            except:
                params['estimated_time'] = None
        return Ticket(**params)
    
    def _create_or_update_category(self, ticket):
        if ticket.category:
            cat = Category.gql('WHERE project = :1 and name = :2', 
                self, ticket.category).get()
            if cat is None:
                cat = Category(project=self, name=ticket.category)
            
            for prop in CAT_DENOMALIZED_PROPERTIES.keys():
                statuses = CAT_DENOMALIZED_PROPERTIES[prop]
                issues = getattr(cat, prop)
                if ticket.status in statuses and ticket.id not in issues:
                    issues.append(ticket.id)
                else:
                    if ticket.id in issues:
                        issues.remove(ticket.id)
            cat.put()
            return cat
        else:
            cat = Category.gql('WHERE project = :1 and open_issues = :2', self, ticket.id).get()
            if cat:
                cat.open_issues.remove(ticket.id)
                cat.put()
            return cat
                
class Milestone(db.Model):
    name = db.StringProperty(required=True)
    due = db.DateProperty(required=True)
    project = db.ReferenceProperty(Project, required=True)

    def tickets(self):
        return Ticket.gql('WHERE milestone = :1 and project = :2', self.name.split('-')[1], self.project.name)

    def q(self):
        return self.name.replace('-', '=')

    def total_time(self):
        sum = 0
        for ticket in self.tickets():
            if ticket.estimated_time:
                sum += ticket.estimated_time
        return sum
    
    def time_left(self):
        sum = 0
        for ticket in self.tickets():
            if ticket.estimated_time and ticket.status != 'Done' and ticket.status != 'WontFix' and ticket.status != 'Fixed':
                sum += ticket.estimated_time
        return sum

    def __str__(self):
        return "%s - %s ( %s )" % (self.project.name, self.name, self.due,)
    
    def datetime(self):
        return datetime.datetime.combine(self.due, datetime.time())
    
    def days_away(self):
        return (self.datetime() - datetime.datetime.now()).days

    def to_dict(self):
           return dict([(p, unicode(getattr(self, p))) for p in self.properties()])

class Holiday(db.Model):
    """Days off that apply to everyone."""
    name = db.StringProperty(required=False)
    start = db.DateTimeProperty(required=True)
    end = db.DateTimeProperty(required=True)
        
    def __str__(self):
        return "%s (%s - %s)" % (self.name, self.start, self.end)
    
    def to_dict(self):
        return dict([(p, unicode(getattr(self, p))) for p in self.properties()])
        
class Committer(db.Model):
    email = db.EmailProperty(required=False)
    # Used for gravatars
    name = db.StringProperty(required=False)
    aliases = db.StringListProperty()
    # Should match names in Google Code

    def obligations(self):
        return Obligation.gql('WHERE committers in :1', self.aliases)
        
    def first_alias(self):
        return self.aliases[0]
        
    def __str__(self):
        if self.name != None:
            return self.name
        else:
            return self.first_alias()
        
    def gravatar(self):
        # import code for encoding urls and generating md5 hashes
        import urllib, hashlib

        # Set your variables here
        email = self.email or '%s@marinemap.org' % (self.name, )
        default = "wavatar"
        size = 40

        # construct the url
        gravatar_url = "http://www.gravatar.com/avatar/" + hashlib.md5(email.lower()).hexdigest() + "?"
        gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
        return gravatar_url

class Obligation(db.Model):
    committers = db.StringListProperty(required=True)
    type = db.StringProperty(required=True, choices=(
        'Vacation', 
        'Meeting', 
        'Other',
    ))
    name = db.StringProperty(required=False)
    start = db.DateTimeProperty(required=True)
    end = db.DateTimeProperty(required=True)

    def to_dict(self):
        d = dict([(p, unicode(getattr(self, p))) for p in self.properties()])
        d['committers'] = [str(c) for c in self.committers]
        return d
      
    @property  
    def formatted_span(self):
        if (self.end - self.start).days > 0:
            return "%s - %s" % (self.start.strftime('%m/%d/%Y'), 
                (self.end - datetime.timedelta(days=1)).strftime('%m/%d/%Y'))
        else:
            return "%s - %s" % (self.start.strftime('%I:%M %p'), 
                self.end.strftime('%I:%M %p'))


    def __str__(self):
        return "%s (%s - %s)" % (self.name, self.start, self.end)
    
    def people(self):
        l = []
        for contributor in self.committers:
            committers = Committer.gql('WHERE aliases = :1', contributor)
            if committers.count() > 0:
                l.append(committers[0])
            else:
                c = Committer(name=contributor, aliases=[contributor])
                c.put()
                l.append(c)
        return l

class Ticket(db.Model):
    id = db.StringProperty(required=True)
    project = db.StringProperty(required=True)
    milestone = db.StringProperty(required=False)
    owner = db.StringProperty(required=False)
    summary = db.StringProperty(required=True)
    description = db.TextProperty(required=True)
    status = db.StringProperty(required=True)
    estimated_time = db.IntegerProperty(required=False)
    category = db.StringProperty(required=False)
    priority = db.StringProperty(required=False)
    labels = db.StringListProperty()
    blocked_on = db.SelfReferenceProperty(required=False)
    phase = db.StringProperty(required=True, default='1')
    updated = db.DateTimeProperty(required=True)
    percent_complete = db.IntegerProperty(required=True, default=0)
    
    def __str__(self):
        return self.id
    
    def to_dict(self):
           d = dict([(p, unicode(getattr(self, p))) for p in self.properties()])
           d['completed'] = self.completed
           d['estimated_time'] = self.estimated_time or False
           return d

    def get_category(self):
        if self.category:
            return Category.gql('WHERE name = :1', self.category).get()
        return None

    def committer(self):
        return Committer.gql('WHERE aliases = :1', self.owner).get()
    
    @property
    def hours(self):
        return int(self.estimated_time)
    
    @property
    def days(self):
        return self.hours / 8.0
    
    @property
    def weeks(self):
        return self.days / 5.0
        
    @property
    def months(self):
        return self.weeks / 4.0
    
    @property
    def years(self):
        return self.months / 12.0
        
    @property
    def completed(self):
        return (self.status in COMPLETED)
            
class Category(db.Model):
    name = db.StringProperty(required=True)
    project = db.ReferenceProperty(Project, required=True)
    wiki = db.LinkProperty(required=False)
    started = db.BooleanProperty(required=True, default=False)
    completed = db.BooleanProperty(required=True, default=False)
    open_issues = db.StringListProperty(required=True, default=[])
    completed_issues = db.StringListProperty(required=True, default=[])
    started_issues = db.StringListProperty(required=True, default=[])
    completed_hours = db.IntegerProperty(required=False)
    total_hours = db.IntegerProperty(required=False)
    
    def put(self, *args, **kwargs):
        
        if len(self.open_issues) == 0:
            self.completed = True

        if len(self.started_issues) == 0 and len(self.completed_issues) == 0:
            self.started = False
        else:
            self.started = True            
            
        super(Category, self).put(*args, **kwargs)
        
    def calculate_hours(self):
        query = GqlQuery("select * from Ticket where estimated_time != :1 and category = :2", None, self.name)
        if self.total_hours is None:
            self.total_hours = 0
        if self.completed_hours is None:
            self.completed_hours = 0
        for t in query:
            self.total_hours += t.estimated_time
        query = GqlQuery("select * from Ticket where estimated_time != :1 and category = :2 and status in :3", None, self.name, COMPLETED)
        for t in query:
            self.completed_hours += t.estimated_time
        self.put()

    def chart_link(self):
        if self.completed_issues != None:
            return "http://chart.apis.google.com/chart?chs=227x105&cht=p&chco=5485CC|80C65A&chds=46.667,%s&chd=t:%s,%s&chdl=%s+hours+left|%s+hours+completed&chp=4.2" % (self.total_hours - self.completed_hours, self.total_hours - self.completed_hours, self.completed_hours, self.total_hours - self.completed_hours, self.completed_hours)
        else:
            return False
